/**
 * Copyright (C) 2011 Angelo Zerr <angelo.zerr@gmail.com> and Pascal Leclercq <pascal.leclercq@gmail.com>
 *
 * All rights reserved.
 *
 * Permission is hereby granted, free  of charge, to any person obtaining
 * a  copy  of this  software  and  associated  documentation files  (the
 * "Software"), to  deal in  the Software without  restriction, including
 * without limitation  the rights to  use, copy, modify,  merge, publish,
 * distribute,  sublicense, and/or sell  copies of  the Software,  and to
 * permit persons to whom the Software  is furnished to do so, subject to
 * the following conditions:
 *
 * The  above  copyright  notice  and  this permission  notice  shall  be
 * included in all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
 * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
 * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package fr.opensagres.xdocreport.document.registry;

import static java.text.MessageFormat.format;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import fr.opensagres.xdocreport.converter.MimeMapping;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.core.io.IEntryInfo;
import fr.opensagres.xdocreport.core.io.XDocArchive;
import fr.opensagres.xdocreport.core.logging.LogUtils;
import fr.opensagres.xdocreport.core.registry.AbstractRegistry;
import fr.opensagres.xdocreport.core.utils.StringUtils;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.discovery.IXDocReportFactoryDiscovery;
import fr.opensagres.xdocreport.document.timing.AlarmTimer;
import fr.opensagres.xdocreport.document.timing.AlarmTimerListener;
import fr.opensagres.xdocreport.document.timing.PooledAlarmTimer;
import fr.opensagres.xdocreport.template.ITemplateEngine;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.cache.ITemplateCacheInfoProvider;

/**
 * Registry for loading and cahing {@link IXDocReport} instances.
 */
public class XDocReportRegistry extends AbstractRegistry<IXDocReportFactoryDiscovery> implements
		ITemplateCacheInfoProvider {
	/**
	 * Logger for this class
	 */
	private static final Logger LOGGER = LogUtils.getLogger(AbstractRegistry.class.getName());

	private static final String FILES_TYPE_ERROR = "Impossible to create report for the input stream. The report loader supports only [{0}] files type.";
	private static final XDocReportRegistry INSTANCE = new XDocReportRegistry();

	private final Collection<IXDocReportFactoryDiscovery> reportFactoryDiscoveries = new ArrayList<IXDocReportFactoryDiscovery>();
	private final Map<String, IXDocReport> cachedReports = new HashMap<String, IXDocReport>();


	public XDocReportRegistry() {
		super(IXDocReportFactoryDiscovery.class);
	}

	public static XDocReportRegistry getRegistry() {
		return INSTANCE;
	}


	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream) throws IOException,
			XDocReportException {
		return loadReport(sourceStream, null, null, null, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, boolean cacheReport)
			throws IOException, XDocReportException {
		return loadReport(sourceStream, null, null, null, cacheReport);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId)
			throws IOException, XDocReportException {
		return loadReport(sourceStream, reportId, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			boolean cacheReport) throws IOException, XDocReportException {
		return loadReport(sourceStream, reportId, null, null, cacheReport);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			ITemplateEngine templateEngine) throws IOException,
			XDocReportException {
		return loadReport(sourceStream, reportId, null, templateEngine, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			ITemplateEngine templateEngine, boolean cacheReport)
			throws IOException, XDocReportException {
		return loadReport(sourceStream, reportId, null, templateEngine,
				cacheReport);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			String templateEngineKind) throws IOException, XDocReportException {
		return loadReport(sourceStream, reportId, templateEngineKind, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			String templateEngineKind, boolean cacheReport) throws IOException,
			XDocReportException {
		return loadReport(sourceStream, reportId, templateEngineKind, null,
				cacheReport);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			TemplateEngineKind templateEngineKind) throws IOException,
			XDocReportException {
		return loadReport(sourceStream, reportId, templateEngineKind, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param reportId
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream, String reportId,
			TemplateEngineKind templateEngineKind, boolean cacheReport)
			throws IOException, XDocReportException {
		return loadReport(sourceStream, reportId, templateEngineKind.name(),
				null, cacheReport);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param templateEngine
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream,
			ITemplateEngine templateEngine) throws IOException,
			XDocReportException {
		return loadReport(sourceStream, templateEngine, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param templateEngine
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream,
			ITemplateEngine templateEngine, boolean cacheReport)
			throws IOException, XDocReportException {
		return loadReport(sourceStream, null, null, templateEngine, cacheReport);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param templateEngine
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream,
			TemplateEngineKind templateEngineKind) throws IOException,
			XDocReportException {
		return loadReport(sourceStream, templateEngineKind, true);
	}

	/**
	 * Load report.
	 * 
	 * @param sourceStream
	 * @param templateEngine
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public IXDocReport loadReport(InputStream sourceStream,
			TemplateEngineKind templateEngineKind, boolean cacheReport)
			throws IOException, XDocReportException {
		return loadReport(sourceStream, null, templateEngineKind.name(), null,
				cacheReport);
	}

	private IXDocReport loadReport(InputStream sourceStream, String reportId,
			String templateEngineKind, ITemplateEngine templateEngine,
			boolean cacheReport) throws IOException, XDocReportException {
		initializeIfNeeded();
		// 2) zip was loaded, create an instance of report
		IXDocReport report = createReport(sourceStream);
		// 3) Update the report id if need.
		if (StringUtils.isEmpty(reportId)) {
			reportId = report.toString();
		}
		report.setId(reportId);
		// 4) Search or set the template engine.
		if (templateEngine == null
				&& StringUtils.isNotEmpty(templateEngineKind)) {
			// Template engine was not forced.
			// Search template engine
			String documentKind = report.getKind();
			templateEngine = TemplateEngineInitializerRegistry.getRegistry()
					.getTemplateEngine(templateEngineKind, documentKind);
			if (templateEngine == null) {
				templateEngine = TemplateEngineInitializerRegistry.getRegistry()
						.getTemplateEngine(templateEngineKind, null);
			}
		}
		report.setTemplateEngine(templateEngine);
		if (cacheReport) {
			registerReport(report);
		}
		return report;
	}

	public IXDocReport createReport(InputStream sourceStream)
			throws IOException, XDocReportException {
		// Load zipped XML document
		XDocArchive documentArchive = XDocArchive.readZip(sourceStream);
		return createReport(documentArchive);
	}

	public IXDocReport createReport(XDocArchive documentArchive)
			throws IOException, XDocReportException {
		initializeIfNeeded();
		for (IXDocReportFactoryDiscovery discovery : reportFactoryDiscoveries) {
			if (discovery.isAdaptFor(documentArchive)) {
				IXDocReport report = discovery.createReport();
				if (report != null) {
					report.setDocumentArchive(documentArchive);
				}
				return report;
			}
		}

		throw new XDocReportException(format(FILES_TYPE_ERROR, getFilesType()));
	}

	private String getFilesType() {
		StringBuilder filesType = new StringBuilder();
		Collection<IXDocReportFactoryDiscovery> discoveries = getReportFactoryDiscoveries();
		for (IXDocReportFactoryDiscovery discovery : discoveries) {
			if (filesType.length() > 0) {
				filesType.append(",");
			}
			filesType.append(discovery.getMimeMapping().getExtension());
		}
		return filesType.toString();
	}

	/**
	 * Register report and throws XDocReportException if it already exists a
	 * report in the registry with the same id .
	 * 
	 * @param report
	 * @throws XDocReportException
	 */
	public void registerReport(IXDocReport report) throws XDocReportException {
		registerReport(report, false);
	}

	/**
	 * Register report.
	 * 
	 * @param report
	 *            the report to register
	 * @param force
	 *            true if report must be forced (if report already exists with
	 *            the same id) and false otherwise (throw XDocReportException if
	 *            report exists with the same id).
	 * @throws XDocReportException
	 */
	public synchronized void registerReport(IXDocReport report, boolean force)
			throws XDocReportException {
		String reportId = report.getId();
		if (StringUtils.isEmpty(reportId)) {
			throw new XDocReportException(
					"Cannot register report. IXDocReport#getId() cannot be empty.");
		}
		if (!force) {
			checkReportId(reportId);
		}
		cachedReports.put(report.getId(), report);
	}

	
	/**
	 * Check if registry can register the report with this id.
	 * 
	 * @param reportId
	 * @throws XDocReportException
	 */
	public void checkReportId(String reportId) throws XDocReportException {
		if (cachedReports.containsKey(reportId)) {
			String msg=String.format("Cannot register report. A report with id=%s already exists in the registry", reportId);
			LOGGER.warning(msg);
			throw new XDocReportException(msg);
		}
	}

	/**
	 * Returns the report identified with the given id.
	 * 
	 * @param reportId
	 * @return
	 */
	public IXDocReport getReport(String reportId) {
		return cachedReports.get(reportId);
	}

	/**
	 * Returns true if report identified with the given id exists in the
	 * registry and false otherwise.
	 * 
	 * @param id
	 * @return
	 */
	public boolean existsReport(String reportId) {
		return cachedReports.containsKey(reportId);
	}

	/**
	 * Unregister report identified with the given id.
	 * 
	 * @param reportId
	 */
	public void unregisterReport(String reportId) {
		if (existsReport(reportId)) {
			cachedReports.remove(reportId);
		}
	}

	/**
	 * Unregister report.
	 * 
	 * @param report
	 */
	public void unregisterReport(IXDocReport report) {
		if (report != null) {
			cachedReports.remove(report.getId());
		}
	}

	/**
	 * Returns list of report factory discoveries.
	 * 
	 * @return
	 */
	public Collection<IXDocReportFactoryDiscovery> getReportFactoryDiscoveries() {
		initializeIfNeeded();
		
		return reportFactoryDiscoveries;
	}

	// /**
	// * Returns true if template engine is default and false otherwise.
	// *
	// * @param templateEngine
	// * @return
	// */
	// public boolean isDefault(ITemplateEngine templateEngine) {
	// ITemplateEngine defaultTemplateEngine = reportLoader
	// .getDefaultTemplateEngine();
	// if (defaultTemplateEngine == null) {
	// return false;
	// }
	// return defaultTemplateEngine.equals(templateEngine);
	// }

	/**
	 * Returns cached report {@link IXDocReport};
	 * 
	 * @return
	 */
	public Collection<IXDocReport> getCachedReports() {
		return Collections.unmodifiableCollection(cachedReports.values());
	}

	public void setClearTimeout(long timeout) {
		PooledAlarmTimer pooledAlarmTimer = new PooledAlarmTimer(timeout);
		pooledAlarmTimer.addAlarmTimerListener(new AlarmTimerListener() {

			public void alarm(AlarmTimer timer) {
				clear();

			}
		});
	}

	/**
	 * Clear the cached reports.
	 */
	public void clear() {
		cachedReports.clear();
	}

	/**
	 * Returns mime mapping for the file extension.
	 * 
	 * @param fileExtension
	 * @return
	 * @throws IOException
	 * @throws XDocReportException
	 */
	public MimeMapping getMimeMapping(String fileExtension) throws IOException,
			XDocReportException {
		initializeIfNeeded();
		for (IXDocReportFactoryDiscovery discovery : reportFactoryDiscoveries) {
			if (discovery.isAdaptFor(fileExtension)) {
				return discovery.getMimeMapping();
			}
		}
		return null;
	}

	/**
	 * Generate unique report id.
	 * 
	 * @param reportId
	 * @return
	 */
	public synchronized String generateUniqueReportId(String reportId) {
		IXDocReport report = getReport(reportId);
		if (report != null) {
			StringBuilder id = new StringBuilder(reportId);
			id.append("_");
			id.append(System.currentTimeMillis());
			return id.toString();
		}
		return reportId;
	}

	public IEntryInfo getTemplateCacheInfo(String reportId, String entryName) {
		IXDocReport report = getReport(reportId);
		if (report == null) {
			return null;
		}
		return report.getPreprocessedDocumentArchive().getEntryInfo(entryName);
	}

	

	@Override
	protected void doDispose() {
		this.reportFactoryDiscoveries.clear();
		this.cachedReports.clear();
	}

	@Override
	protected boolean registerInstance(IXDocReportFactoryDiscovery instance) {
		reportFactoryDiscoveries.add(instance);
		return true;
	}
}
